﻿// Copyright (C) 2009 Choonghwan Lee (linjus.lee@gmail.com)
//
// This file is part of amanda.
//
// amanda is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// amanda is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with amanda. If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.IO;
using System.ComponentModel;
using System.Windows.Threading;

namespace manager
{
	/// <summary>
	/// Interaction logic for MainWindow.xaml
	/// </summary>
	public partial class MainWindow : Window
	{
		private BackgroundWorker backworker;
		private LibraryInfo libraryInfo;

		public MainWindow()
		{
			Global.MainWindow = this;

			InitializeComponent();

			try {
				Configuration.Initialize();
				this.Initialize();
			}
			catch (Exception e) {
				MessageBox.Show("Initialization failed: " + e.Message);
			}

			this.Loaded += new RoutedEventHandler(MainWindow_Loaded);
		}

		void MainWindow_Loaded(object sender, RoutedEventArgs e)
		{
			this.OpenWizard();
		}

		private void Initialize()
		{
			this.backworker = new BackgroundWorker();
			this.backworker.WorkerReportsProgress = true;
			this.backworker.WorkerSupportsCancellation = true;
			this.backworker.DoWork += new DoWorkEventHandler(worker_DoWork);
			this.backworker.ProgressChanged += new ProgressChangedEventHandler(worker_ProgressChanged);
			this.backworker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);

			this.listboxLog.ItemsSource = new LogMessages();

			foreach (var device in Configuration.Devices)
				this.AddLog(LogMessage.Kind.INFO, "Device '" + device.Name + "' was detected.");
			foreach (var library in Configuration.Libraries)
				this.AddLog(LogMessage.Kind.INFO, "Library '" + library.Name + "' was detected.");
		}

		private void AddLogUnsafe(LogMessage msg)
		{
			LogMessages msgs = (LogMessages)this.listboxLog.ItemsSource;
			msgs.Add(msg);
			this.listboxLog.ScrollIntoView(msg);
		}

		private void AddLog(LogMessage.Kind kind, string messsage)
		{
			LogMessage msg = new LogMessage(kind, messsage);

			if (this.listviewLibrary.CheckAccess())
				this.AddLogUnsafe(msg);
			else {
				Dispatcher.Invoke(DispatcherPriority.Send, (Action)delegate
				{
					this.AddLogUnsafe(msg);
				});
			}
		}

		struct WorkerData
		{
			public int NumTasks;
			public int NumSuccessful;
			public int NumFailed;
		}

		void worker_DoWork(object sender, DoWorkEventArgs e)
		{
			BackgroundWorker worker = (BackgroundWorker)sender;
			e.Result = this.Commit(this.libraryInfo, worker, e);
		}

		void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
		{
			this.progressbarWorker.Value = e.ProgressPercentage;
		}

		void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
		{
			if (e.Cancelled)
				this.textblockStatus.Text = "Canceled";
			else
				this.textblockStatus.Text = "Finished";
			this.progressbarWorker.Visibility = Visibility.Collapsed;

			if (e.Cancelled)
				this.AddLog(LogMessage.Kind.INFO, "Canceled");
			else {
				WorkerData result = (WorkerData)e.Result;
				string msg = "Among " + result.NumTasks + " tracks, " +
					result.NumSuccessful + " tracks were synchronized, " +
					result.NumFailed + " tracks were failed.";
				this.AddLog(LogMessage.Kind.SUCCESS, msg);
			}

			this.buttonCommit.IsEnabled = true;
			this.buttomStop.IsEnabled = false;
			if (e.Cancelled)
				this.IsEnabled = true;
		}

		private Track CreateTrack(LibraryInfo libraryinfo, Foobar2000.Track07 track)
		{
			Encoding encoding = Encoding.ETC;
			string codec = track.FormatTitle("%codec%");
			switch (codec.ToLower()) {
				case "mp3": encoding = Encoding.MP3; break;
				case "ogg": case "vorbis": encoding = Encoding.OGG; break;
				case "flac": encoding = Encoding.FLAC; break;
			}

			string artist = track.FormatTitle("%artist%");
			string album = track.FormatTitle("%album%");
			string trackname = track.FormatTitle("%title%");
			string fullpath = track.Path;
			SynchronizedStatus status = SynchronizedStatus.NOT_YET;

			return new Track(artist, album, trackname, encoding, fullpath, status, libraryinfo.Device);
		}

		private Track FindTrack(Tracks library, string devicepath)
		{
			Track victim = null;
			foreach (Track track in library) {
				if (track.DevicePath == devicepath) {
					victim = track;
					break;
				}
			}
			return victim;
		}

		private void ReadDeviceRecursive(DirectoryInfo di, Tracks library)
		{
			foreach (FileInfo child in di.GetFiles()) {
				string ext = child.Extension.Substring(1);
				if (!EncodingExtension.Extensions.Contains(ext)) continue;

				if (this.FindTrack(library, child.FullName) == null) {
					Track t = new Track(child.FullName);
					library.Add(t);
				}
			}

			foreach (DirectoryInfo child in di.GetDirectories())
				this.ReadDeviceRecursive(child, library);
		}

		public int ReadDevice(LibraryInfo libraryinfo)
		{
			Tracks library = this.listviewLibrary.ItemsSource as Tracks;
			if (library == null) library = new Tracks();

			DirectoryInfo di = new DirectoryInfo(libraryinfo.DeviceRootPath);
			this.ReadDeviceRecursive(di, library);

			this.listviewLibrary.ItemsSource = library;
			return library.Count;
		}

		public int ReadFoobar2000(LibraryInfo libraryinfo)
		{
			Tracks library = this.listviewLibrary.ItemsSource as Tracks;
			if (library == null) library = new Tracks();

			int count = 0;

			var helper = new Foobar2000Helper.ApplicationHelper07Class();
			if (!helper.Running)
				throw new Exception("Foobar2000 is not running");
			var application = (Foobar2000.Application07)helper.Server;

			bool found = false;
			foreach (Foobar2000.Playlist07 playlist in application.Playlists) {
				if (playlist.Name == libraryinfo.Name) {
					found = true;

					Foobar2000.Tracks07 tracks = playlist.GetTracks(null);
					foreach (Foobar2000.Track07 ftrack in tracks) {
						Track track = this.CreateTrack(libraryinfo, ftrack);
						if (this.CheckSynchronized(libraryinfo, track))
							track.SyncStatus = SynchronizedStatus.UP_TO_DATE;
						Track victim = this.FindTrack(library, track.DevicePath);
						if (victim != null)
							library.Remove(victim);
						library.Add(track);
						count++;
					}

					this.listviewLibrary.ItemsSource = library;
					break;
				}
			}

			if (!found)
				throw new Exception("Foobar2000 does not have the corresponding playlist.");

			this.buttonCommit.IsEnabled = true;
			this.buttonRemoveJunk.IsEnabled = true;
			return count;
		}

		public void ResetLibrary()
		{
			this.buttonCommit.IsEnabled = false;
			this.buttonRemoveJunk.IsEnabled = false;

			this.listviewLibrary.ItemsSource = null;
			this.libraryInfo = null;
		}

		private void buttonCommit_Click(object sender, RoutedEventArgs e)
		{
			this.buttonCommit.IsEnabled = false;
			this.buttomStop.IsEnabled = true;
			this.textblockStatus.Text = "Committing...";
			this.progressbarWorker.Value = 0;
			this.progressbarWorker.Visibility = Visibility.Visible;

			this.backworker.RunWorkerAsync();
		}

		private void buttomStop_Click(object sender, RoutedEventArgs e)
		{
			this.textblockStatus.Text = "Stopping...";
			this.IsEnabled = false;

			this.backworker.CancelAsync();
		}

		private void buttonRemoveJunk_Click(object sender, RoutedEventArgs e)
		{
			this.RemoveJunk();
		}

		private bool CheckSynchronized(LibraryInfo libraryinfo, Track track)
		{
			track.DevicePath = this.GuessFinalPath(libraryinfo, track);
			if (File.Exists(track.DevicePath))
				return true;
			return false;
		}

		private string GetPurePath(Track track)
		{
			string sourcepath = track.Path;
			if (sourcepath.StartsWith("file://"))
				sourcepath = sourcepath.Substring("file://".Length);
			return sourcepath;
		}

		private string GuessFinalPath(LibraryInfo libraryinfo, Track track)
		{
			string sourcepath = this.GetPurePath(track);

			System.Diagnostics.Debug.Assert(libraryinfo.PlaylistRootPath.EndsWith("\\"));
			if (!sourcepath.StartsWith(libraryinfo.PlaylistRootPath, true, System.Globalization.CultureInfo.CurrentCulture)) return null;
			string relative = sourcepath.Substring(libraryinfo.PlaylistRootPath.Length);

			string[] split = relative.Split(new char[] { '\\' });
			foreach (RuleAction a in track.Actions) {
				if (a is IRenameAction) {
					string[] next;
					((IRenameAction)a).Rename(track, split, out next);
					split = next;
				}
			}

			string finalfname = libraryinfo.DeviceRootPath + string.Join("\\", split);
			return finalfname;
		}

		private void MoveToFinalPath(LibraryInfo libraryinfo, Track track, string workingpath)
		{
			string finalpath = this.GuessFinalPath(libraryinfo, track);
			int fnamebegins = finalpath.LastIndexOf('\\');
			if (fnamebegins == -1)
				throw new Exception("Path seems wrong '" + workingpath + "'");

			string devicefolder = finalpath.Substring(0, fnamebegins);
			Directory.CreateDirectory(devicefolder);

			if (track.ForceSync)
				File.Delete(finalpath);
			File.Move(workingpath, finalpath);
		}

		private bool NeedSync(Track track)
		{
			if (track.SyncStatus == SynchronizedStatus.JUNK) return false;
			if (track.ForceSync) return true;
			if (track.SyncStatus == SynchronizedStatus.UP_TO_DATE) return false;
			return true;
		}

		private WorkerData Commit(LibraryInfo libraryinfo, BackgroundWorker worker, DoWorkEventArgs args)
		{
			WorkerData result = new WorkerData();
			foreach (Track track in (Tracks)this.listviewLibrary.ItemsSource) {
				if (!this.NeedSync(track)) continue;
				result.NumTasks++;
			}

			ClearAction clear = new ClearAction();
			foreach (Track track in (Tracks)this.listviewLibrary.ItemsSource) {
				if (worker.CancellationPending) {
					args.Cancel = true;
					break;
				}

				if (!this.NeedSync(track)) continue;

				try {
					string sourcepath = this.GetPurePath(track);
					string workingpath = null;

					// The order is quite important. For example, transcoding should be performed as early as possible.
					clear.Commit();
					foreach (RuleAction a in track.Actions) {
						if (a is CopyAction) {
							CopyAction action = (CopyAction)a;
							action.Commit(sourcepath, out workingpath);
						}
						else if (a is TranscodingAction) {
							TranscodingAction action = (TranscodingAction)a;
							string transcodedpath;
							action.Commit(workingpath, libraryinfo.Device, track, out transcodedpath);
							workingpath = transcodedpath;
						}
						else if (a is ArtworkEmbedAction) {
							ArtworkEmbedAction action = (ArtworkEmbedAction)a;
							action.Commit(workingpath, sourcepath);
						}
						else if (a is RomanizationAction) {
							RomanizationAction action = (RomanizationAction)a;
							action.Commit(workingpath, track);
						}
						else if (a is FlattenStructureAction) {
							FlattenStructureAction action = (FlattenStructureAction)a;
						}
					}

					this.MoveToFinalPath(libraryinfo, track, workingpath);

					track.ForceSync = false;
					track.SyncStatus = SynchronizedStatus.UP_TO_DATE;
					result.NumSuccessful++;
					this.AddLog(LogMessage.Kind.SUCCESS, "Track '" + track.TrackName + "' was synchronized.");
				}
				catch (Exception e) {
					result.NumFailed++;
					this.AddLog(LogMessage.Kind.FAIL, "Track '" + track.TrackName + "' was failed: " + e.Message);
				}
				finally {
					worker.ReportProgress((int)((float)(result.NumSuccessful + result.NumFailed) / result.NumTasks * 100));
				}
			}
			return result;
		}

		private void RemoveJunk()
		{
			int numFailed = 0, numSucceeded = 0;
			List<Track> removed = new List<Track>();

			foreach (Track track in (Tracks)this.listviewLibrary.ItemsSource) {
				if (track.SyncStatus != SynchronizedStatus.JUNK) continue;

				try {
					File.Delete(track.DevicePath);

					++numSucceeded;
					removed.Add(track);
					this.AddLog(LogMessage.Kind.SUCCESS, "Junk '" + track.DevicePath + "' was removed.");
				}
				catch (Exception e) {
					++numFailed;
					this.AddLog(LogMessage.Kind.FAIL, "Junk '" + track.DevicePath + "' was not removed: " + e.Message);
				}
			}

			foreach (Track t in removed)
				((Tracks)this.listviewLibrary.ItemsSource).Remove(t);

			string msg =
				numSucceeded + " junks were removed, " +
				numFailed + " junks were not removed.";
			this.AddLog(LogMessage.Kind.SUCCESS, msg);
		}

		private void listviewLibrary_MouseDoubleClick(object sender, MouseButtonEventArgs e)
		{
			DependencyObject dep = (DependencyObject)e.OriginalSource;
			while ((dep != null) && !(dep is ListViewItem))
				dep = VisualTreeHelper.GetParent(dep);
			if (dep == null) return;

			Track track = (Track)this.listviewLibrary.ItemContainerGenerator.ItemFromContainer(dep);
			track.ForceSync = !track.ForceSync;
		}

		private void buttonAbout_Click(object sender, RoutedEventArgs e)
		{
			About about = new About();
			about.Show();
		}

		private void buttonOpen_Click(object sender, RoutedEventArgs e)
		{
			this.OpenWizard();
		}

		private void OpenWizard()
		{
			Wizard wizard = new Wizard();

			bool? result = wizard.ShowDialog();
			if (!result.HasValue || !result.Value)
				App.Current.Shutdown();
			else
				this.libraryInfo = wizard.Data.LibraryInfo;
		}
	}
}
